package com.ar4j.handler;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import net.sf.cglib.proxy.MethodProxy;

import com.ar4j.ActiveRecord;
import com.ar4j.ActiveRecordContext;
import com.ar4j.ActiveRecordMetadata;
import com.ar4j.pagination.ISortField;
import com.ar4j.pagination.PaginationAndSort;
import com.ar4j.pagination.ISortField.SortDirection;
import com.ar4j.sql.ActiveRecordRowMapper;
import com.ar4j.sql.ExtendedMapSqlParameterSource;
import com.ar4j.sql.SqlUtils;

/**
 * A handler for active record find operations
 */
public class FindMethodHandler implements IMethodHandler {
  private String queryPrefix;
  private ReentrantLock prefixCreateLock = new ReentrantLock();
  
  @SuppressWarnings("unchecked")
  @Override
  public Object intercept(ActiveRecordMetadata metadata, ActiveRecordContext context, Object target, Method method, Object[] args, MethodProxy proxy) 
    throws Throwable {
    
    // compose the prefix if its the first time we are running
    if(queryPrefix == null) {
      prefixCreateLock.lock();
      try {
        if(queryPrefix == null) {
          Collection<String> columns = new ArrayList<String>();
          for(String property : metadata.getBackingStoreReadableProperties()) {
            columns.add(metadata.getPropertyColumnNameMap().get(property));
          }
          
          queryPrefix = composeQueryPrefix(metadata.getTableName(), columns);
        }
      } finally {
        prefixCreateLock.unlock();
      }
    }
    
    // make sure we have a template first
    if(context.getSimpleTemplate() == null) {
      throw new IllegalStateException("Cannot execute find on an active record that does not have a datasource set");
    }
    
    // get parameters first
    Map<String, Object> parameters = (Map<String, Object>) args[0];
    PaginationAndSort pagination = PaginationAndSort.NONE;
    boolean lockForUpdate = false;
    
    if(args.length > 1) {
      pagination = (PaginationAndSort) args[1];
    }
    
    if(args.length > 2) {
      lockForUpdate = (Boolean) args[2];
    }
    
    // build field list and 
    Collection<String> fields = new HashSet<String>();
    Collection<String> collectionFields = new HashSet<String>();
    HandlerUtils.scanParametersForFields(parameters, metadata, fields, collectionFields);
    
    // build parameters and get query
    String sql = composeFullQuery(fields, collectionFields, metadata.getPropertyColumnNameMap(), pagination, lockForUpdate);
    ExtendedMapSqlParameterSource source = new ExtendedMapSqlParameterSource(parameters, metadata.getPropertyTypeConvertorMap());
    if(pagination.getStartRow() > 0 || pagination.getMaxRows() < Integer.MAX_VALUE) {
      source.addValue("startRow", pagination.getStartRow());
      source.addValue("maxRows", pagination.getMaxRows());
    }
    
    List<Object> results = context.getSimpleTemplate().query(sql, new ActiveRecordRowMapper(metadata, context), source);
    for(Object result : results) {
      ActiveRecord<?> resultRecord = (ActiveRecord<?>) result;
      resultRecord.getContext().setValuesFrom(context);
    }
    
    return results;
  }

  private String composeQueryPrefix(String tableName, Collection<String> columns) {
    StringBuilder sql = new StringBuilder();
    sql.append("SELECT ");
    SqlUtils.appendCommaDelimitedColumnList(sql, "T", columns);
    sql.append(" FROM ");
    sql.append(tableName);
    sql.append(" T ");
    
    return sql.toString();
  }
  
  private String composeFullQuery(Collection<String> fields, Collection<String> collectionFields, Map<String, String> fieldToColumnMap, PaginationAndSort pagination, boolean forUpdate) {
    StringBuilder sql = new StringBuilder();
    sql.append(queryPrefix);
    
    if(fields.size() > 0) {
      SqlUtils.appendWhereClause(sql, fields, collectionFields, "T", fieldToColumnMap);
    }
    
    if(pagination.getSortFields().size() > 0) {
      sql.append(" ORDER BY ");
      boolean first = true;
      for(ISortField field : pagination.getSortFields()) {
        if(!first) {
          sql.append(", ");
        } else {
          first = false;
        }
        
        sql.append(fieldToColumnMap.get(field.getFieldName()));
        if(field.getSortDirection() == SortDirection.ASCENDING) {
          sql.append(" asc ");
        } else {
          sql.append(" desc ");
        }
      }
    }
    
    if(pagination.getStartRow() > 0 || pagination.getMaxRows() < Integer.MAX_VALUE) {
      sql.append(" LIMIT :startRow, :maxRows");
    }
    
    if(forUpdate) {
      sql.append(" FOR UPDATE ");
    }
    
    return sql.toString();
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
}
